/*
 * Copyright © 2017 eqxiu.com 北京中网易企秀科技有限公司  All rights reserved.
 */

package cn.hermit.doclet;

import cn.hermit.data.json.JSONParser;
import cn.hermit.data.json.factory.OrderedJSONFactory;
import cn.hermit.data.json.model.JSONArray;
import cn.hermit.data.json.model.JSONDocument;
import cn.hermit.data.json.model.JSONFactory;
import cn.hermit.data.json.model.JSONObject;
import cn.hermit.nio.http.Http;
import cn.hermit.util.StringUtils;
import com.sun.javadoc.ClassDoc;
import com.sun.javadoc.FieldDoc;
import com.sun.javadoc.MethodDoc;
import com.sun.javadoc.ParamTag;
import com.sun.javadoc.Parameter;
import com.sun.javadoc.ParameterizedType;
import com.sun.javadoc.RootDoc;
import com.sun.javadoc.Tag;
import com.sun.javadoc.Type;

import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.file.FileVisitResult;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;

/**
 * @author JackGao 高建国
 * @description //TODO 设计说明
 * @date 6/6/2017
 */
public class DubboDocGenerator extends DocGenerator {

    private JSONFactory jsonFactory = new OrderedJSONFactory();

    private String project;

    private String packagePattern;

    private String apiClassPattern = ".+\\.api\\.\\w+Api$";

    private String swaggerFile;

    @Override
    protected void generate(RootDoc root) {
        JSONObject swagger = null;
        try {
            if (StringUtils.isBlank(swaggerFile)) {
                swagger = jsonFactory.createObject();
                initSwagger(swagger, jsonFactory);
            } else {
                swagger = (JSONObject) JSONParser.getInstance().parse(new FileReader(swaggerFile));
            }
            Map<String, ClassDoc> classDocMap = new HashMap<>();
            for (ClassDoc classDoc : root.classes()) {
                if (packagePattern != null) {
                    String pkgName = classDoc.containingPackage().name();
                    if (!pkgName.matches(packagePattern)) {
                        continue;
                    }
                }
                classDocMap.put(classDoc.qualifiedTypeName(), classDoc);
            }

            JSONObject pathsObj = (JSONObject) swagger.getValue("paths");
            for (String clzName : classDocMap.keySet()) {
                if (!clzName.matches(apiClassPattern)) {
                    continue;
                }
                ClassDoc classDoc = classDocMap.get(clzName);

                JSONObject tagObj = generateClassTag(classDoc);
                JSONArray tagsArr = (JSONArray) swagger.getValue("tags");
                jsonFactory.arrayValue(tagsArr, tagObj);

                MethodDoc[] methodDocs = classDoc.methods();
                if (methodDocs.length == 0) {
                    continue;
                }
                for (MethodDoc methodDoc : methodDocs) {
                    if (methodDoc.isStatic() || !methodDoc.isPublic()) {
                        continue;
                    }
                    String path = '/' + clzName + '/' + methodDoc.name();
                    JSONObject pathObj = jsonFactory.createObject();
                    jsonFactory.objectEntry(pathsObj, path, pathObj);

                    JSONObject methodObj = jsonFactory.createObject();
                    jsonFactory.objectEntry(pathObj, Http.Verb.POST.name().toLowerCase(), methodObj);


                    JSONArray tagsStrArr = jsonFactory.createArray();
                    jsonFactory.objectEntry(methodObj, "tags", tagsStrArr);
                    jsonFactory.arrayValue(tagsStrArr, tagObj.getValue("name").toString());

                    String operationId = classDoc.name() + '_' + methodDoc.name();
                    jsonFactory.objectEntry(methodObj, "operationId", operationId);


                    String methodDesc = null;
                    if (methodDoc.firstSentenceTags().length > 0) {
                        methodDesc = methodDoc.firstSentenceTags()[0].text().trim();
                    } else {
                        methodDesc = methodDoc.name() + "  Description";
                    }
                    String summary = methodDesc;
                    jsonFactory.objectEntry(methodObj, "summary", summary);
                    jsonFactory.objectEntry(methodObj, "description", methodDesc);

                    String[] produces = null;
                    Type returnType = methodDoc.returnType();

                    JSONObject schemaObj = generateScheme(returnType, classDocMap, swagger);
                    JSONDocument typeObj = schemaObj.getValue("type");
                    if (typeObj == null) {
                        JSONDocument refObj = schemaObj.getValue("$ref");
                        if (refObj != null) {
                            produces = new String[]{Http.MEDIA_TYPE_APPLICATION_JSON};
                        } else {
                            //TODO impossible
                            produces = new String[]{Http.MEDIA_TYPE_TEXT_PLAIN};
                        }
                    } else {
                        String schemaType = typeObj.toString();
                        if ("array".equals(schemaType) || "object".equals(schemaType)) {
                            produces = new String[]{Http.MEDIA_TYPE_APPLICATION_JSON};
                        } else {
                            produces = new String[]{Http.MEDIA_TYPE_TEXT_PLAIN};
                        }
                    }

                    JSONObject responsesObj = jsonFactory.createObject();
                    jsonFactory.objectEntry(methodObj, "responses", responsesObj);

                    JSONObject okResponseObj = jsonFactory.createObject();
                    jsonFactory.objectEntry(responsesObj, "200", okResponseObj);
                    String returnDesc = null;
                    Tag[] tags = methodDoc.tags("return");
                    if (tags == null || tags.length == 0) {
                        returnDesc = classDoc.name() + '.' + methodDoc.name() + ".response";
                    } else {
                        for (Tag tag : tags) {
                            returnDesc = tag.text().trim();
                        }
                    }
                    jsonFactory.objectEntry(okResponseObj, "description", returnDesc);
                    jsonFactory.objectEntry(okResponseObj, "schema", schemaObj);

                    if (produces != null && produces.length > 0) {
                        JSONArray producesArr = jsonFactory.createArray();
                        jsonFactory.objectEntry(methodObj, "produces", producesArr);
                        for (String produce : produces) {
                            jsonFactory.arrayValue(producesArr, produce);
                        }
                    }

                    Parameter[] parameters = methodDoc.parameters();
                    if (parameters == null || parameters.length == 0) {
                        //nothing to do
                    } else {
                        String[] consumers = null;
                        JSONArray parametersArr = jsonFactory.createArray();
                        jsonFactory.objectEntry(methodObj, "parameters", parametersArr);
                        for (Parameter parameter : parameters) {
                            JSONObject parameterObj = jsonFactory.createObject();
                            jsonFactory.arrayValue(parametersArr, parameterObj);
                            String name = parameter.name();
                            boolean required = false;
                            String _default = null;
                            String paramDesc = getParameterDescription(parameter, methodDoc);
                            if (StringUtils.isBlank(paramDesc)) {
                                paramDesc = methodDoc.containingClass().name() + '.' + methodDoc.name() + '.' + name;
                            }

                            if (!StringUtils.isBlank(_default)) {
                                jsonFactory.objectEntry(parameterObj, "default", _default);
                            }
                            if (required) {
                                jsonFactory.objectEntry(parameterObj, "required", required);
                            }

                            jsonFactory.objectEntry(parameterObj, "name", name);
                            jsonFactory.objectEntry(parameterObj, "description", paramDesc);

                            Type paramType = parameter.type();
                            JSONObject paramSchemaObj = generateScheme(paramType, classDocMap, swagger);
                            JSONDocument paramTypeObj = paramSchemaObj.getValue("type");
                            if (paramTypeObj == null) {
                                JSONDocument paramRefObj = paramSchemaObj.getValue("$ref");
                                if (paramRefObj != null) {
                                    if (parameters.length == 1) {
                                        consumers = new String[]{Http.MEDIA_TYPE_APPLICATION_JSON};
                                        jsonFactory.objectEntry(parameterObj, "name", "body");
                                        jsonFactory.objectEntry(parameterObj, "in", Http.ParamIn.BODY.getValue());
                                        jsonFactory.objectEntry(parameterObj, "schema", paramSchemaObj);
                                    } else {
                                        jsonFactory.objectEntry(parameterObj, "in", Http.ParamIn.FORM.getValue());
                                    }
                                } else {
                                    //TODO impossible
                                    jsonFactory.objectEntry(parameterObj, "type", "string");
                                    jsonFactory.objectEntry(parameterObj, "in", Http.ParamIn.FORM.getValue());
                                }
                            } else {
                                String schemaType = paramTypeObj.toString();
                                if ("array".equals(schemaType)) {
                                    JSONObject itemsObj = (JSONObject) paramSchemaObj.getValue("items");
                                    if (parameters.length == 1) {
                                        consumers = new String[]{Http.MEDIA_TYPE_APPLICATION_JSON};
                                        jsonFactory.objectEntry(parameterObj, "name", "body");
                                        jsonFactory.objectEntry(parameterObj, "in", Http.ParamIn.BODY.getValue());
                                        jsonFactory.objectEntry(parameterObj, "schema", paramSchemaObj);
                                    } else {
                                        jsonFactory.objectEntry(parameterObj, "in", Http.ParamIn.FORM.getValue());
                                        jsonFactory.objectEntry(parameterObj, "type", "array");
                                        jsonFactory.objectEntry(parameterObj, "items", itemsObj);
                                    }
                                } else if ("object".equals(schemaType)) {
                                    if (parameters.length == 1) {
                                        consumers = new String[]{Http.MEDIA_TYPE_APPLICATION_JSON};
                                        jsonFactory.objectEntry(parameterObj, "name", "body");
                                        jsonFactory.objectEntry(parameterObj, "in", Http.ParamIn.BODY.getValue());
                                        jsonFactory.objectEntry(parameterObj, "schema", paramSchemaObj);
                                    } else {
                                        jsonFactory.objectEntry(parameterObj, "in", Http.ParamIn.FORM.getValue());
                                        jsonFactory.objectEntry(parameterObj, "schema", paramSchemaObj);
                                    }
                                } else {
                                    jsonFactory.objectEntry(parameterObj, "type", schemaType);
                                    jsonFactory.objectEntry(parameterObj, "in", Http.ParamIn.FORM.getValue());
                                }
                            }
                        }
                        if (consumers == null || consumers.length == 0) {
                            consumers = new String[]{Http.MEDIA_TYPE_APPLICATION_FORM_URLENCODED};
                        }
                        JSONArray consumesArr = jsonFactory.createArray();
                        jsonFactory.objectEntry(methodObj, "consumers", consumesArr);
                        for (String consumer : consumers) {
                            jsonFactory.arrayValue(consumesArr, consumer);
                        }
                    }
                }
            }
            writeSwagger(swagger);
        } catch (Exception e) {
            logger.log(Level.SEVERE, e.getMessage(), e);
        }
    }

    private JSONObject generateScheme(Type type, Map<String, ClassDoc> classDocMap, JSONObject swagger) {
        JSONObject schemaObj = jsonFactory.createObject();
        boolean isArray = false;
        if (!StringUtils.isBlank(type.dimension())) {
            isArray = true;
        }
        if (type.isPrimitive()) {
            Http.ParamRealType paramRealType = Http.ParamRealType.getRealType(type.qualifiedTypeName());
            if (paramRealType == null) {
                if (isArray) {
                    jsonFactory.objectEntry(schemaObj, "type", "array");
                    JSONObject itemsObj = jsonFactory.createObject();
                    jsonFactory.objectEntry(schemaObj, "items", itemsObj);
                    jsonFactory.objectEntry(itemsObj, "type", "string");
                } else {
                    jsonFactory.objectEntry(schemaObj, "type", "string");
                }
            } else {
                String swaggerType = Http.ParamRealType.getSwaggerType(paramRealType);
                String format = Http.ParamRealType.getDataFormat(paramRealType);
                if (isArray) {
                    jsonFactory.objectEntry(schemaObj, "type", "array");
                    JSONObject itemsObj = jsonFactory.createObject();
                    jsonFactory.objectEntry(schemaObj, "items", itemsObj);
                    jsonFactory.objectEntry(itemsObj, "type", swaggerType);
                    if (!StringUtils.isBlank(format)) {
                        jsonFactory.objectEntry(itemsObj, "format", format);
                    }
                } else {
                    jsonFactory.objectEntry(schemaObj, "type", swaggerType);
                    if (!StringUtils.isBlank(format)) {
                        jsonFactory.objectEntry(schemaObj, "format", format);
                    }
                }
            }
            return schemaObj;
        }
        String typeName = type.qualifiedTypeName();
        ClassDoc classDoc = type.asClassDoc();
        if (typeName.endsWith(".PageDTO") || isCollection(classDoc)) {
            ParameterizedType parameterizedType = type.asParameterizedType();
            if (parameterizedType != null) {
                Type[] types = parameterizedType.typeArguments();
                if (types != null && types.length > 0) {
                    classDoc = types[0].asClassDoc();
                }
            }
            isArray = true;
        }
        String className = classDoc.qualifiedTypeName();
        ClassDoc cachedClassDoc = classDocMap.get(className);
        if (cachedClassDoc == null) {
            Http.ParamRealType paramRealType = Http.ParamRealType.getRealType(classDoc.qualifiedTypeName());
            if (paramRealType == null) {
                // TODO less common condition
            } else {
                String swaggerType = Http.ParamRealType.getSwaggerType(paramRealType);
                String format = Http.ParamRealType.getDataFormat(paramRealType);
                if (isArray) {
                    jsonFactory.objectEntry(schemaObj, "type", "array");
                    JSONObject itemsObj = jsonFactory.createObject();
                    jsonFactory.objectEntry(schemaObj, "items", itemsObj);
                    jsonFactory.objectEntry(itemsObj, "type", swaggerType);
                    if (!StringUtils.isBlank(format)) {
                        jsonFactory.objectEntry(itemsObj, "format", format);
                    }
                } else {
                    jsonFactory.objectEntry(schemaObj, "type", swaggerType);
                    if (!StringUtils.isBlank(format)) {
                        jsonFactory.objectEntry(schemaObj, "format", format);
                    }
                }
            }
        } else {
            JSONObject schemaDefinitionObj = getSchemaDefinition(classDoc, classDocMap, swagger);
            if (schemaDefinitionObj == null) {
                //impossible
            } else {
                if (isArray) {
                    jsonFactory.objectEntry(schemaObj, "type", "array");
                    JSONObject itemsObj = jsonFactory.createObject();
                    jsonFactory.objectEntry(schemaObj, "items", itemsObj);
                    jsonFactory.objectEntry(itemsObj, "$ref", "#/definitions/" + classDoc.name());
                } else {
                    jsonFactory.objectEntry(schemaObj, "$ref", "#/definitions/" + classDoc.name());
                }
            }
        }
        if (schemaObj.size() == 0) {
            jsonFactory.objectEntry(schemaObj, "type", "string");
        }
        return schemaObj;
    }

    private JSONObject generateClassTag(ClassDoc classDoc) {
        JSONObject tagObj = jsonFactory.createObject();
        jsonFactory.objectEntry(tagObj, "name", classDoc.name());
        String classDesc = null;
        if (classDoc.firstSentenceTags().length > 0) {
            classDesc = classDoc.firstSentenceTags()[0].text().trim();
        } else {
            classDesc = classDoc.name() + "  Description";
        }
        jsonFactory.objectEntry(tagObj, "description", classDesc);
        return tagObj;
    }

    private void writeSwagger(JSONObject swagger) {
        PrintWriter writer = null;
        try {
            if (StringUtils.isBlank(buildDir)) {
                writer = new PrintWriter(System.out);
            } else {
                File docsDir = new File(buildDir + "/docs");
                if (!docsDir.exists()) {
                    docsDir.mkdirs();
                }
                writer = new PrintWriter(new FileWriter(new File(docsDir, "swagger.json")));
            }
            writer.print(swagger.toFormatString());
            writer.flush();
        } catch (IOException e) {
            logger.log(Level.SEVERE, e.getMessage(), e);
        } finally {
            if (writer != null && buildDir != null) {
                writer.close();
            }
        }

    }

    @Override
    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
        String filePath = file.toFile().getCanonicalPath();
        if (filePath.endsWith(".java")) {
            argumentList.add(filePath);
        } else if (filePath.endsWith("swagger.json")) {
            swaggerFile = filePath;
        }
        return FileVisitResult.CONTINUE;
    }


    private String getParameterDescription(Parameter parameter, MethodDoc methodDoc) {
        String paramDesc = null;
        ParamTag[] paramTags = methodDoc.paramTags();
        for (ParamTag paramTag : paramTags) {
            if (parameter.name().equals(paramTag.parameterName())) {
                paramDesc = paramTag.parameterComment();
                break;
            }
        }
        return paramDesc;
    }

    private boolean isCollection(ClassDoc classDoc) {
        boolean isCollection = false;
        for (ClassDoc clzDoc : classDoc.interfaces()) {
            if (clzDoc.qualifiedTypeName().equals(Collection.class.getName())) {
                isCollection = true;
                break;
            }
        }
        return isCollection;
    }

    private JSONObject getSchemaDefinition(ClassDoc classDoc, Map<String, ClassDoc> classDocMap, JSONObject swagger) {
        String schemaName = classDoc.simpleTypeName();
        JSONObject definitionsObj = (JSONObject) swagger.getValue("definitions");
        JSONObject definitionObj = (JSONObject) definitionsObj.getValue(schemaName);
        if (definitionObj == null) {
            definitionObj = jsonFactory.createObject();
            jsonFactory.objectEntry(definitionsObj, schemaName, definitionObj);
            jsonFactory.objectEntry(definitionObj, "type", "object");
            JSONObject propertiesObj = jsonFactory.createObject();
            jsonFactory.objectEntry(definitionObj, "properties", propertiesObj);
            FieldDoc[] fieldDocs = classDoc.fields(false);
            if (fieldDocs != null && fieldDocs.length > 0) {
                //TODO
                for (FieldDoc fieldDoc : fieldDocs) {
                    if (fieldDoc.isStatic() || !fieldDoc.isPrivate()) {
                        continue;
                    }
                    boolean isArray = false;
                    Type fieldType = fieldDoc.type();
                    if (!StringUtils.isBlank(fieldType.dimension())) {
                        isArray = true;
                    }
                    String typeName = null;
                    if (fieldType.isPrimitive()) {
                        typeName = fieldType.qualifiedTypeName();

                    } else {
                        ClassDoc fieldClz = fieldDoc.type().asClassDoc();
                        if (!StringUtils.isBlank(fieldClz.dimension())) {
                            isArray = true;
                        } else if (isCollection(fieldClz)) {
                            ParameterizedType parameterizedType = fieldClz.asParameterizedType();
                            if (parameterizedType != null) {
                                ClassDoc clzDoc = classDocMap.get(parameterizedType.qualifiedTypeName());
                                if (clzDoc != null) {
                                    fieldClz = clzDoc;
                                }
                            }
                            isArray = true;
                        }
                        if (!fieldClz.qualifiedTypeName().equals(fieldType.qualifiedTypeName())) {
                            JSONObject schemaObj = getSchemaDefinition(fieldClz, classDocMap, swagger);
                            if (schemaObj != null) {
                                JSONObject paramObj = jsonFactory.createObject();
                                if (isArray) {
                                    jsonFactory.objectEntry(paramObj, "type", "array");
                                    JSONObject itemsObj = jsonFactory.createObject();
                                    jsonFactory.objectEntry(paramObj, "items", itemsObj);
                                    jsonFactory.objectEntry(itemsObj, "$ref", fieldClz.name());
                                } else {
                                    jsonFactory.objectEntry(paramObj, "$ref", fieldClz.name());
                                }
                                jsonFactory.objectEntry(propertiesObj, fieldDoc.name(), paramObj);
                                continue;
                            }
                        }
                        typeName = fieldClz.qualifiedTypeName();
                    }
                    Http.ParamRealType paramRealType = Http.ParamRealType.getRealType(typeName);
                    if (paramRealType != null) {
                        String dataType = Http.ParamRealType.getSwaggerType(paramRealType);
                        String dataFormat = Http.ParamRealType.getDataFormat(paramRealType);
                        JSONObject paramObj = jsonFactory.createObject();
                        if (isArray) {
                            jsonFactory.objectEntry(paramObj, "type", "array");
                            JSONObject itemsObj = jsonFactory.createObject();
                            jsonFactory.objectEntry(paramObj, "items", itemsObj);
                            jsonFactory.objectEntry(itemsObj, "type", dataType);
                            if (!StringUtils.isBlank(dataFormat)) {
                                jsonFactory.objectEntry(itemsObj, "format", dataFormat);
                            }
                            JSONObject xmlObj = jsonFactory.createObject();
                            jsonFactory.objectEntry(paramObj, "xml", xmlObj);
                            //TODO
                            jsonFactory.objectEntry(xmlObj, "name", fieldDoc.name());
                            jsonFactory.objectEntry(xmlObj, "wrapped", true);
                        } else {
                            jsonFactory.objectEntry(paramObj, "type", dataType);
                            if (!StringUtils.isBlank(dataFormat)) {
                                jsonFactory.objectEntry(paramObj, "format", dataFormat);
                            }
                        }

                        jsonFactory.objectEntry(propertiesObj, fieldDoc.name(), paramObj);

                    } else {
                        //TODO
                    }
                }
            }
            JSONObject xmlObj = jsonFactory.createObject();
            jsonFactory.objectEntry(definitionObj, "xml", xmlObj);
            jsonFactory.objectEntry(xmlObj, "name", schemaName);
        }
        return definitionObj;
    }

    private void initSwagger(JSONObject swagger, JSONFactory jsonFactory) {
        jsonFactory.objectEntry(swagger, "swagger", "2.0");

        JSONObject info = jsonFactory.createObject();
        jsonFactory.objectEntry(swagger, "info", info);
        jsonFactory.objectEntry(info, "version", "1.0.0");
        jsonFactory.objectEntry(info, "title", "Swagger Sample");
        jsonFactory.objectEntry(info, "description", "Swagger Sample");
        jsonFactory.objectEntry(info, "termsOfService", "http://eqxiu.com/terms/");
        JSONObject contact = jsonFactory.createObject();
        jsonFactory.objectEntry(info, "contact", contact);
        jsonFactory.objectEntry(contact, "email", "admin@eqxiu.com");
        JSONObject license = jsonFactory.createObject();
        jsonFactory.objectEntry(info, "license", license);
        jsonFactory.objectEntry(license, "name", "EQXIU");
        jsonFactory.objectEntry(license, "url", "http://eqxiu.com/licenses/LICENSE-2.0.html");


        jsonFactory.objectEntry(swagger, "host", "");
        jsonFactory.objectEntry(swagger, "basePath", "");

        JSONArray tags = jsonFactory.createArray();
        jsonFactory.objectEntry(swagger, "tags", tags);

        JSONArray schemes = jsonFactory.createArray();
        jsonFactory.objectEntry(swagger, "schemes", schemes);
        jsonFactory.arrayValue(schemes, "http");

        JSONObject paths = jsonFactory.createObject();
        jsonFactory.objectEntry(swagger, "paths", paths);

        JSONObject definitions = jsonFactory.createObject();
        jsonFactory.objectEntry(swagger, "definitions", definitions);
    }

    public String getProject() {
        return project;
    }

    public void setProject(String project) {
        this.project = project;
        sourcePath = this.project + "/src/main/java";
    }

    public String getPackagePattern() {
        return packagePattern;
    }

    public void setPackagePattern(String packagePattern) {
        this.packagePattern = packagePattern;
    }

    public String getApiClassPattern() {
        return apiClassPattern;
    }

    public void setApiClassPattern(String apiClassPattern) {
        this.apiClassPattern = apiClassPattern;
    }

    public static void main(String[] args) {
        DubboDocGenerator generator = new DubboDocGenerator();
        generator.setProject("/Users/JackGao/eqxiu/ws/user-ws/mylab/examples/dubbo/api");
        generator.setBuildDir("/Users/JackGao/eqxiu/ws/user-ws/mylab/examples/dubbo/api/build");
        generator.setPackagePattern("cn\\.hermit\\.examples\\.dubbo\\.\\w+");
        generator.setApiClassPattern("cn\\.hermit\\.examples\\.dubbo\\.api\\.\\w+Api");
        generator.generate();
    }
}
